﻿using System;
using System.IO;
using System.Xml;
using System.Linq;
using System.Text;
using System.Threading;
using System.Globalization;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using NazTek.Utility.Extensions.Misc;
using System.Runtime.Serialization.Formatters.Soap;
using System.Runtime.Serialization.Formatters.Binary;
using Settings = NazTek.Utility.Properties.Settings;

namespace NazTek.Utility.Extensions.SimpleType
{
    /// <summary> 
    /// Extends string
    /// </summary> 
    /// <remarks></remarks> 
    public static class StringExtension
    {
        /// <summary>
        /// Converts a string to boolean.
        /// </summary>
        /// <param name="data">The value to convert.</param>
        /// <returns>True when value is in (1, y, yes, true), otherwise false.</returns>
        public static bool ToBool(this string data)
        {
            if (string.IsNullOrWhiteSpace(data))
                return false;
            switch (data.ToLower())
            {
                case "1":
                case "y":
                case "yes":
                case "true":
                    return true;
                default:
                    return false;
            }
        }
        /// <summary> 
        /// Converts to an item in the generically specified Enum type, based on string name match 
        /// </summary> 
        /// <typeparam name="T">The Generic type to search for the corresponding item match</typeparam> 
        /// <param name="enumItem">Corresponding string value of the enum item to search for</param> 
        /// <returns>The matched enum item</returns> 
        /// <remarks></remarks> 
        /// <example>
        /// <code>
        /// // context
        /// internal enum Gender
        /// {
        ///     Male,
        ///     Female
        /// }
        /// 
        /// // usage
        /// Enum e = "1".ToEnum&lt;Gender&gt;(false); // Female
        /// Enum e = "Female".ToEnum&lt;Gender&gt;(false); // Female
        /// Enum e = "female".ToEnum&lt;Gender&gt;(false); // null
        /// Enum e = "female".ToEnum&lt;Gender&gt;(true); // Exception
        /// Enum e = "2".ToEnum&lt;Gender&gt;(false); // null
        /// Enum e = "2".ToEnum&lt;Gender&gt;(true); // Exception
        /// </code>
        /// </example>
        public static T ToEnum<T>(this string enumItem)
        {
            var genericType = typeof(T);
            // validate
            Parameter.ValidateToEnum<string, T>(enumItem, genericType.FullName, ToEnumValidationItem.SourceNotEmpty,
                ToEnumValidationItem.DestinationIsEnum);
            // map supplied value
            if (Enum.IsDefined(genericType, enumItem.Trim()))
                return (T)Enum.Parse(genericType, enumItem);
            // map underlying value
            switch (Enum.GetUnderlyingType(genericType).FullName)
            {
                case Constant.QtnByte:
                    byte byteEnumValue;
                    var isByte = byte.TryParse(enumItem, out byteEnumValue);
                    return ToEnum<byte, T>(isByte, enumItem, byteEnumValue);
                case Constant.QtnSbyte:
                    sbyte sbyteEnumValue;
                    var isSByte = sbyte.TryParse(enumItem, out sbyteEnumValue);
                    return ToEnum<sbyte, T>(isSByte, enumItem, sbyteEnumValue);
                case Constant.QtnShort:
                    short shortEnumValue;
                    var isShort = Int16.TryParse(enumItem, out shortEnumValue);
                    return ToEnum<Int16, T>(isShort, enumItem, shortEnumValue);
                case Constant.QtnUshort:
                    ushort ushortEnumValue;
                    var isUShort = UInt16.TryParse(enumItem, out ushortEnumValue);
                    return ToEnum<UInt16, T>(isUShort, enumItem, ushortEnumValue);
                case Constant.QtnInt:
                    int intEnumValue;
                    var isInt = int.TryParse(enumItem, out intEnumValue);
                    return ToEnum<int, T>(isInt, enumItem, intEnumValue);
                case Constant.QtnUint:
                    uint uintEnumValue;
                    var isUInt = uint.TryParse(enumItem, out uintEnumValue);
                    return ToEnum<uint, T>(isUInt, enumItem, uintEnumValue);
                case Constant.QtnLong:
                    long longEnumValue;
                    var isLong = long.TryParse(enumItem, out longEnumValue);
                    return ToEnum<long, T>(isLong, enumItem, longEnumValue);
                case Constant.QtnUlong:
                    ulong ulongEnumValue;
                    var isULong = ulong.TryParse(enumItem, out ulongEnumValue);
                    return ToEnum<ulong, T>(isULong, enumItem, ulongEnumValue);
                default:
                    throw new CustomException(string.Format(CultureInfo.CurrentCulture,
                        Settings.Default.Msg_UndefinedEnum, enumItem, genericType));
            }
        }
        /// <summary> 
        /// Locates a corresponding item in the generically specified Enum type 
        /// </summary> 
        /// <typeparam name="T">The Generic type to search for the corresponding item match</typeparam> 
        /// <param name="enumItemDescription">String attribute value of the enum item to search for</param> 
        /// <param name="defaultValue">Return value when no match is found</param>
        /// <returns>The matched enum item</returns> 
        /// <remarks></remarks> 
        /// <example>
        /// <code>
        /// // context
        /// internal enum Gender
        /// {
        ///     [Description("M")]
        ///     [Description("ShortDescription", "Man")]
        ///     Male,
        ///     [Description("F")]
        ///     [Description("ShortDescription", "Woman")]
        ///     Female
        /// }
        /// 
        /// // usage
        /// Enum e = "M".ToEnumFromDescripion&lt;Gender&gt;(true); // Male
        /// Enum e = "Woman".ToEnumFromDescripion&lt;Gender&gt;(true); // Female
        /// Enum e = "X".ToEnumFromDescripion&lt;Gender&gt;(true); // Exception
        /// Enum e = "X".ToEnumFromDescripion&lt;Gender&gt;(false); // null
        /// </code>
        /// </example>
        public static T ToEnumFromDescription<T>(this string enumItemDescription, T defaultValue)
        {
            Parameter.ValidateEnum<T>();
            var genericType = typeof(T);
            var array = Enum.GetValues(genericType);
            foreach (var enumValue in from T enumValue in array
                let list = new DescriptionAttributeList(enumValue as Enum)
                where list.ContainsDescription(enumItemDescription)
                select enumValue)
                return enumValue;
            return defaultValue;
        }
        /// <summary> 
        /// Converts a string to an instance of a specified type 
        /// </summary> 
        /// <typeparam name="T">The type to convert the string to</typeparam> 
        /// <param name="serializedObject">string to convert to an object</param> 
        /// <param name="formatOption">Format option</param>
        /// <returns>Either an instance of the specified type or nothing</returns> 
        /// <remarks>This method performs the reverse operation of <see cref="ObjectExtension.ToString(object, FormatOption)"/></remarks> 
        /// <example>
        /// <code>
        /// // assuming the object was serialized using Base64Binary format option
        /// MyType t = myString.ToType&lt;MyType&gt;(FormatOption.Base64Binary);
        /// </code>
        /// </example>
        /// <seealso cref="ObjectExtension.ToString(object, FormatOption)"/>
        public static T ToType<T>(this string serializedObject, FormatOption formatOption) where T : class
        {
            if (!Parameter.IsValid(serializedObject))
                return null;
            switch (formatOption)
            {
                case FormatOption.Base32Binary:
                    return ToType<T>(serializedObject, new BinaryFormatter());
                case FormatOption.Base32Soap:
                    return ToType<T>(serializedObject, new SoapFormatter());
                case FormatOption.Base64Binary:
                    return ToType<T>(DefaultEncoding.GetString(Convert.FromBase64String(serializedObject)),
                        new BinaryFormatter());
                case FormatOption.Base64Soap:
                    return ToType<T>(DefaultEncoding.GetString(Convert.FromBase64String(serializedObject)),
                        new SoapFormatter());
                case FormatOption.Pox:
                    return ToType<T>(serializedObject);
                default:
                    throw new CustomException(Settings.Default.Msg_UnsupportedSerializationFormat);
            }
        }
        /// <summary>
        /// Converts a given data element to proper case.
        /// </summary>
        /// <param name="data">Data element to change case of.</param>
        /// <returns>Proper cased data element.</returns>
        public static string ToProperCase(this string data)
        {
            return Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(data);
        }
        /// <summary> 
        /// Compresses string to base 64
        /// </summary> 
        /// <param name="data">The string to compress</param> 
        /// <returns>Compressed string</returns> 
        /// <remarks></remarks> 
        /// <example>
        /// <code>
        /// string compressedString = myString.Compress();
        /// </code>
        /// </example>
        public static string Compress(this string data)
        {
            return !Parameter.IsValid(data) ? "" : Convert.ToBase64String(DefaultEncoding.GetBytes(data).Compress());
        }
        /// <summary>
        /// Determines if a data element is encrypted.
        /// </summary>
        /// <param name="data">The data element to check for encryption.</param>
        /// <param name="regExPattern">The RexEx pattern the plain text data is expected to conform to.</param>
        /// <returns>True when data value does not match the specified RexEx pattern.</returns>
        public static bool IsDataEncrypted(this string data, string regExPattern)
        {
            return !Regex.IsMatch(data, regExPattern);
        }

        /// <summary> 
        /// Decompresses string from base 64
        /// </summary> 
        /// <param name="data">The string to decompress</param> 
        /// <returns>Decompressed string</returns> 
        /// <remarks>This method performs the reverse operation of <see cref="Compress(string)"/></remarks> 
        /// <example>
        /// <code>
        /// string uncompressedString = compressedString.Decompress();
        /// </code>
        /// </example>
        /// <seealso cref="Compress(string)"/>
        public static string Decompress(this string data)
        {
            return !Parameter.IsValid(data) ? "" : DefaultEncoding.GetString(Convert.FromBase64String(data).Decompress());
        }

        /// <summary> 
        /// Gets OS default encoded memory stream 
        /// </summary> 
        /// <param name="data">The string to convert to stream</param> 
        /// <returns>Hydrated memory stream</returns> 
        /// <remarks></remarks> 
        /// <example>
        /// <code>
        /// MemoryStream stream = myString.ToMemoryStream();
        /// </code>
        /// </example>
        public static MemoryStream ToMemoryStream(this string data)
        {
            return ToMemoryStream(data, DefaultEncoding);
        }
        /// <summary> 
        /// Gets default encoded memory stream 
        /// </summary> 
        /// <param name="data">The string to convert to stream</param> 
        /// <param name="encoding">Encodes the stream with</param>
        /// <returns>Hydrated memory stream</returns> 
        /// <remarks></remarks> 
        /// <example>
        /// <code>
        /// MemoryStream stream = myString.ToMemoryStream(Encoding.ASCII);
        /// </code>
        /// </example>
        public static MemoryStream ToMemoryStream(this string data, Encoding encoding)
        {
            Parameter.Validate("encoding", encoding);
            return new MemoryStream(encoding.GetBytes(data));
        }

        /// <summary>
        /// Generates a hash for the given plain text value and returns a base64-encoded result. Before the hash is computed, supplied salt
        /// is appended to the plain text. This salt is also stored at the end of the hash value, so it can be used later for hash verification.
        /// </summary>
        /// <param name="plaintext">Plaintext value to hash.</param>
        /// <param name="encoding">Character encoding to use to generate byte stream, defaults to OS default.</param>
        /// <param name="hashAlgorithm">Algorithm to use to compute hash from interim byte array, defaults to <see cref="MD5CryptoServiceProvider"/>.</param>
        /// <param name="salt">Random security bytes, http://en.wikipedia.org/wiki/Salt_bits .</param>
        /// <returns>Hash value formatted as a base64-encoded string.</returns>
        /// <remarks>Null parameter values are replaced with non-null defaults.</remarks>
        public static string GetHashValue(this string plaintext, Encoding encoding = null,
            HashAlgorithm hashAlgorithm = null, byte[] salt = null)
        {
            Parameter.Validate("plaintext", plaintext);
            if (!Parameter.IsValid(encoding))
                encoding = DefaultEncoding;
            if (!Parameter.IsValid(hashAlgorithm))
                hashAlgorithm = DefaultHashAlgorithm;
            if (!Parameter.IsValid(salt))
                salt = Constant.RandomSalt;

            // Convert plain text into a byte array.
            var plainTextBytes = encoding.GetBytes(plaintext);

            // Allocate array, which will hold plain text and salt.
            var plainTextWithSaltBytes = new byte[plainTextBytes.Length + salt.Length];

            // Copy plain text bytes into resulting array.
            int i;
            for (i = 0; i <= plainTextBytes.Length - 1; i++)
                plainTextWithSaltBytes[i] = plainTextBytes[i];

            // Append salt bytes to the resulting array.
            for (i = 0; i <= salt.Length - 1; i++)
                plainTextWithSaltBytes[plainTextBytes.Length + i] = salt[i];

            // Compute hash value of our plain text with appended salt.
            var hashBytes = hashAlgorithm.ComputeHash(plainTextWithSaltBytes);

            // Create array which will hold hash and original salt bytes.
            var hashWithSaltBytes = new byte[hashBytes.Length + salt.Length];

            // Copy hash bytes into resulting array.
            for (i = 0; i <= hashBytes.Length - 1; i++)
                hashWithSaltBytes[i] = hashBytes[i];

            // Append salt bytes to the result.
            for (i = 0; i <= salt.Length - 1; i++)
                hashWithSaltBytes[hashBytes.Length + i] = salt[i];

            // Convert to base64-encoded string and return the result.
            return Convert.ToBase64String(hashWithSaltBytes);
        }

        /// <summary>
        /// Replaces parameters, enclosed in square brackets, in a string
        /// </summary>
        /// <param name="source">The string to replace parameters from</param>
        /// <param name="args">The key/value pairs used to replace keys in the string with corresponding values</param>
        /// <returns>The string with parameters replaced with values</returns>
        public static string ReplaceParams(this string source, List<KeyValuePair<string, string>> args)
        {
            args.ForEach(arg => source = source.Replace(arg.Key, arg.Value));
            return source;
        }
        /// <summary>
        /// Sequentially replaces parameters, enclosed in square brackets, in a string with supplied values
        /// </summary>
        /// <param name="source">The string to replace parameters from</param>
        /// <param name="args">The values used to replace keys, in square brackets, in the string</param>
        /// <returns>The string with parameters replaced with values</returns>
        public static string ReplaceParams(this string source, params string[] args)
        {
            foreach (var t in args)
            {
                var firstParamStartIdx = source.IndexOf('[');
                if (firstParamStartIdx < 0) 
                    continue;
                var firstParamEndIdx = source.IndexOf(']', firstParamStartIdx);
                source = source.Replace(source.Substring(firstParamStartIdx, firstParamEndIdx - firstParamStartIdx + 1), t);
            }
            return source;
        }

        /// <summary>
        /// Gets the OS default encoding
        /// </summary>
        private static Encoding DefaultEncoding { get { return Encoding.Default; } }
        /// <summary>
        /// Gets an instance of <see cref="MD5CryptoServiceProvider"/>
        /// </summary>
        private static HashAlgorithm DefaultHashAlgorithm { get { return new MD5CryptoServiceProvider(); } }
        /// <summary>
        /// Gets matching enum item to the specified enum value based on underlying type
        /// </summary>
        /// <param name="isUnderlyingEnumType">Identifies whether the underlying type of the specified enum value is the same as the underlying type of the specified enum type</param>
        /// <param name="enumItemOrDescripion">Enum string value</param>
        /// <param name="underlyingEnumValue">Enum value of the underlying enum type</param>
        /// <returns>Matched enum value or null if no match is found</returns>
        private static TDestination ToEnum<TSource, TDestination>(bool isUnderlyingEnumType, string enumItemOrDescripion, TSource underlyingEnumValue)
        {
            var enumType = typeof(TDestination);
            if (!isUnderlyingEnumType)
                throw new CustomException(string.Format(CultureInfo.CurrentCulture,
                    Settings.Default.Msg_UndefinedEnum, enumItemOrDescripion, enumType.FullName));
            Parameter.ValidateToEnum<TSource, TDestination>(underlyingEnumValue,
                Enum.GetUnderlyingType(enumType).FullName, ToEnumValidationItem.ExpectedUnderlyingDesinationType,
                ToEnumValidationItem.SourceDefinedInDestinaion);
            return (TDestination)Enum.Parse(enumType, underlyingEnumValue.ToString());
        }
        /// <summary> 
        /// Deserializes an object from it's soap string representation 
        /// </summary> 
        /// <typeparam name="T">The type to deserialize to</typeparam> 
        /// <param name="serializedObject">Serialized object string</param> 
        /// <param name="formatter">Soap/Binary formatter</param>
        /// <returns>Either an instance of the specified type or nothing</returns> 
        /// <remarks>If the strign can't be converted to an instance of the specified type, nothing is returned</remarks> 
        private static T ToType<T>(string serializedObject, IFormatter formatter) where T : class
        {
            using (var stream = ToMemoryStream(serializedObject))
                return formatter.Deserialize(stream) as T;
        }
        /// <summary>
        /// Deserializes an object from it's pox string representation 
        /// </summary>
        /// <typeparam name="T">The type to deserialize to</typeparam>
        /// <param name="serializedObject">Pox representation of the object</param>
        /// <returns>Either an instance of the specified type or nothing</returns>
        private static T ToType<T>(string serializedObject) where T : class
        {
            using (var stream = new MemoryStream(Encoding.Unicode.GetBytes(serializedObject)))
            {
                using (var reader = XmlDictionaryReader.CreateTextReader(stream, Encoding.Unicode,
                    new XmlDictionaryReaderQuotas(), null))
                {
                    var dataContractSerializer = new DataContractSerializer(typeof (T));
                    return dataContractSerializer.ReadObject(reader) as T;
                }
            }
        }
    }
}